Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js SSR example #1596

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

Next.js SSR example #1596

wants to merge 16 commits into from

Conversation

balegas
Copy link
Contributor

@balegas balegas commented Aug 29, 2024

Items example using SSR.

Load the entire shape on the server and send it down to the client, so that the client gets the initial state of the shape on first response and can resume replication from the tip of the shape stream.

First time with nextjs, I still don't know how some things work. I suppose this is good forn an example. I'm happy to receive feedback from nextjs experts :D

Copy link
Contributor

@KyleAMathews KyleAMathews left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments.

Don't you want to use either https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props or https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props here? Right now I don't see that any data is being cached during server rendering.

EDIT: Oh I see... this is RSC stuff

examples/nextjs-ssr-example/app/Home.tsx Outdated Show resolved Hide resolved
examples/nextjs-ssr-example/app/Home.tsx Outdated Show resolved Hide resolved
examples/nextjs-ssr-example/app/shape.tsx Outdated Show resolved Hide resolved
packages/typescript-client/src/client.ts Outdated Show resolved Hide resolved
examples/nextjs-ssr-example/app/page.tsx Outdated Show resolved Hide resolved
@balegas balegas force-pushed the balegas/ssr-example branch from 64f3703 to 2d933e8 Compare October 3, 2024 00:10
Copy link

netlify bot commented Oct 3, 2024

Deploy Preview for electric-next ready!

Name Link
🔨 Latest commit dc9da79
🔍 Latest deploy log https://app.netlify.com/sites/electric-next/deploys/673b23a1b567730008140042
😎 Deploy Preview https://deploy-preview-1596--electric-next.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@samwillis
Copy link
Contributor

First, I think there is going to be two ways people want to do SSR with useShape:

  1. Fetch the full snapshot on the server, render it, and then include the snapshot in the HTML sent to the client. This allows the client to just continue from where it was in the stream (just as this PR does). This would make sense for smaller shapes - such as a single issue + comments on a detail view.

  2. Render a subset of the data on the server, not include the snapshot in the HTML sent to the client, and then fetch the full snapshot on the client. This would make sense for larger shapes - such as a very long list of issues, just rendering the "above the fold" on the server. Refetching already rendered data is ok in this situation.

I think we want to provide tooling for both of these cases.


On 1 - snapshot of the shape and resume

While the pattern in this PR works, having to pass the shape snapshot to the component using useShape via props is a little messy. It would be great if we could provide a cleaner API that "just works" with SSR.

I think this is doable if we create a <ShapeSsrStore> (or similar) component that the user wraps their app in. This component will keep track of all useShape calls done during SSR and then inject a <script> tag with the JSON of those shapes as a global (keyed by shapeId?). This can then be read back and used when hydrating the useShapes on the client.

We are somewhat restricted as we cant use a react context, but something like this POC is possible: https://github.com/samwillis/ssr-shape-poc (I've purposely not used any electric, just wanted to test the concept for injection)

By the user adding <ShapeSsrStore>, all useShapes would SSR with automatic resume. (We could make this opt in with a flag to the useShape options)

This is also not tied to Next, and should work with all React SSR frameworks.

image

On 2 - a minimal subset on the server

I think we should add a getServerSnapshot option to useShape that is only run on the server. The user can configure this to do a direct Postgres query to fore a subset of data for SSR. This would override the shape URL can just any snapshotting from option 1 above.

useShape({
  url: `http://localhost:3000/v1/shape/foo`,
  getServerSnapshot: () => {
    // Do and return a Postgres query
  },
})

@thruflo
Copy link
Contributor

thruflo commented Oct 8, 2024

Great stuff @samwillis :)

On 2, I wonder if there's a way of supporting order_by and limit of the shape results? An arbitrary query is powerful but means the user needs to format the results in the same way as the shape data. This will become harder as and when we support nested associations. So is there an approach where the user could order/limit/(filter?) the shape when convenient and then drop down to a function when that's not expressive enough?

@balegas balegas force-pushed the balegas/ssr-example branch from 02ffd0d to dd64a75 Compare October 25, 2024 11:59
Copy link
Contributor Author

@balegas balegas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samwillis inspired on your example and Nextjs docs, I gave another try. The idea here is to have a context around the component that takes the server-sent data from props and initializes the shapes on the client --- totally transparent on the client component!

Left some comments

examples/nextjs-ssr-example/app/ssr-shapes-provider.ts Outdated Show resolved Hide resolved
packages/react-hooks/src/react-hooks.tsx Show resolved Hide resolved
examples/nextjs-ssr-example/app/page.tsx Outdated Show resolved Hide resolved
packages/typescript-client/src/shape.ts Show resolved Hide resolved
examples/nextjs-ssr-example/app/Home.tsx Outdated Show resolved Hide resolved
@balegas
Copy link
Contributor Author

balegas commented Oct 25, 2024

Render a subset of the data on the server,

@samwillis, @thruflo my initial idea was to call the database directly on the server. This is a powerful pattern to show, but because of the problems with ordering and paging, I thought it would be better to build just on Shapes. The benefit of Shapes is that requests go through the cache and doesn't put load on the server db.

A naive way of rendering the subset of data on the server is to just drop the data that doesn't fit into the initial page rendering :)

@balegas balegas force-pushed the balegas/ssr-example branch from 887f224 to 2d8330e Compare November 9, 2024 00:14
@balegas
Copy link
Contributor Author

balegas commented Nov 12, 2024

@samwillis , I did a DX refactoring addressing your concerns with it being simple to specify the shapes across server and client. Have a look at it or ping me to see together.

I also want to replace the base nextjs example with this one (cc @KyleAMathews)

@balegas balegas self-assigned this Nov 12, 2024
@balegas balegas force-pushed the balegas/ssr-example branch from 0eccfa5 to 1b3880d Compare November 13, 2024 00:04
@balegas balegas force-pushed the balegas/ssr-example branch from 1b3880d to dc9da79 Compare November 18, 2024 11:23
@balegas balegas marked this pull request as ready for review November 18, 2024 11:23
Copy link
Contributor

@@ -62,7 +62,7 @@ jobs:
- name: Deploy NextJs example
working-directory: examples/nextjs-example
working-directory: examples/nextjs-ssr-example
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a naming nit: there's no need to suffix with -example, that's communicated by the parent folder name, so I would suggest examples/nextjs-ssr.

@@ -0,0 +1,22 @@
# Next.js SSR example
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, the README for an example should ideally communicate a bit about what it's trying to show you, i.e.: what is the example, why is it interesting, what can you achieve with the pattern it demonstrates, etc. Plus perhaps some signposts into key parts of the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will work on that when we decide to publish

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're purple nowadays/

} from '@electric-sql/client'
import React from 'react'
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector.js'

type UnknownShape = Shape<Row<unknown>>
type UnknownShapeStream = ShapeStream<Row<unknown>>
export type SerializedShape = {
offset: Offset
shapeHandle: string | undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another very minor nit but seeings how this is in the react-hooks library not just the example, I've been trying to move us towards just using handle rather than shapeHandle.

Below when you getSerialisedShape you can also just read it from shape.handle rather than shapeStream.shapeHandle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, i started this a while back, things like the offset were not public at the time. Will cleanup.

const shape = getShape(shapeStream)
return {
shapeHandle: shapeStream.shapeHandle,
offset: shapeStream.offset,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does offset work? Is the property not lastOffset?

@@ -245,6 +245,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
return this.#shapeHandle
}

get offset() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is duplicated now -- we already expose lastOffset as a public getter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants